package com.shchekoldin.juliet.json.codec.encoder;

import com.shchekoldin.juliet.json.JsonFieldConverter;
import com.shchekoldin.juliet.json.annotation.ToJson;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.*;

public class Encoder {

    private static final String NULL_VALUE = "null";

    private static final String EMPTY_OBJECT = "{}";

    private static final String EMPTY_ARRAY = "[]";

    private static final String HIBERNATE_PROXY_CLASS_POSTFIX = "_$$_javassist_";

    private static SimpleDateFormat dateFormatter;

    private String currentProfile = null;

    private Map<String, JsonFieldConverter> convertersHash = new HashMap<String, JsonFieldConverter>();

    static {
        dateFormatter = new SimpleDateFormat("MMM dd, yyyy HH:mm:ss");
        dateFormatter.setDateFormatSymbols(new DateFormatSymbols(Locale.ROOT));
    }

    private static PrimitiveClassType getPrimitiveClassType(Class clazz) {
        boolean isSimple = clazz.isPrimitive() || clazz.isAssignableFrom(Byte.class)
            || clazz.isAssignableFrom(Short.class) || clazz.isAssignableFrom(Boolean.class)
            || clazz.isAssignableFrom(Integer.class) || clazz.isAssignableFrom(Long.class)
            || clazz.isAssignableFrom(Float.class) || clazz.isAssignableFrom(Double.class)
            || clazz.isAssignableFrom(BigDecimal.class);

        if ( isSimple ) return PrimitiveClassType.SIMPLE;

        if ( clazz.isEnum() ) return PrimitiveClassType.ENUM;

        boolean isEscapable = clazz.isAssignableFrom(Character.class)
            || clazz.isAssignableFrom(String.class);

        if ( isEscapable ) return PrimitiveClassType.ESCAPABLE;

        if ( java.util.Date.class.isAssignableFrom(clazz) ) {
            return PrimitiveClassType.DATE;
        }

        return PrimitiveClassType.NONE;
    }

    private static Object getFieldValue(Field field, Object obj) {
        String fieldName = field.getName();
        String getterPrefix = (field.getType().isAssignableFrom(Boolean.class) ? "is" : "get");
        String getterName = getterPrefix + StringUtil.capitalize(fieldName);

        try {
            Method getter = obj.getClass().getMethod(getterName);
            getter.setAccessible(true);
            return getter.invoke(obj);
        } catch (NoSuchMethodException noSuchMethodException) {
            try {
                field.setAccessible(true);
                return field.get(obj);
            } catch (Exception e) {
                return null;
            }
        } catch (Exception e) {
            return null;
        }
    }

    private static String getValueNameFromMethodName(String methodName) {
        if ( (methodName.length() < 4) || (!methodName.substring(0, 3).equals("get")) ) {
            return methodName;
        }

        return StringUtil.decapitalize(methodName.substring(3));
    }

    private static String getMemberName(AccessibleObject member, ToJson toJson) {
        if (!toJson.jsonName().equals("")) {
            return toJson.jsonName();
        }

        if ( member instanceof Field ) {
            return ((Field) member).getName();
        } else if ( member instanceof Method ) {
            String methodName = ((Method) member).getName();
            return getValueNameFromMethodName(methodName);
        }

        throw new EncodeException();
    }

    private static Object getMemberValue(AccessibleObject member, Object obj) {
        if ( member instanceof Field ) {
            return getFieldValue((Field) member, obj);
        } else if ( member instanceof Method ) {
            try {
                member.setAccessible(true);
                return ((Method) member).invoke(obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        throw new EncodeException();
    }

    private static Field[] getAllFields(Class clazz) {
        List<Field> result = new ArrayList<Field>();
        Class currentClass = clazz;

        while (currentClass != Object.class) {
            result.addAll(Arrays.asList(currentClass.getDeclaredFields()));
            currentClass = currentClass.getSuperclass();
        }

        return result.toArray(new Field[result.size()]);
    }

    private ProfileSearchResult getMemberProfile(ToJson toJson) {
        ProfileSearchResult result = new ProfileSearchResult();

        if ( currentProfile == null ) return result;

        String[] profiles = toJson.profiles();

        if (profiles.length == 0) {
            throw new EncodeException();
        }

        for (String profile : profiles) {
            if (profile.equals(currentProfile)) {
                result.setProfile(profile);
                return result;
            }
        }

        result.setSkipCurrentMember(true);
        return result;
    }

    private JsonFieldConverter getJsonFieldConverter(Class<? extends JsonFieldConverter> converterClass) {
        String converterClassName = converterClass.getName();

        if (convertersHash.containsKey(converterClassName)) {
            return convertersHash.get(converterClassName);
        }

        JsonFieldConverter converter = null;

        try {
            converter = converterClass.newInstance();
            convertersHash.put(converterClassName, converter);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return converter;
    }

    private void fillValuesHashWithMemberData(Object obj, AccessibleObject[] members,
        HashMap<String, String> valuesHash, Map<String, Boolean> includeOnlyHash) {
        for (AccessibleObject member : members) {
            ToJson toJson = member.getAnnotation(ToJson.class);

            if ( toJson == null ) continue;

            ProfileSearchResult profileSearchResult = getMemberProfile(toJson);

            if ( profileSearchResult.isSkipCurrentMember() ) continue;

            String memberName = getMemberName(member, toJson);

            if ((includeOnlyHash != null) && !includeOnlyHash.containsKey(memberName)) {
                continue;
            }

            Object memberValue = getMemberValue(member, obj);

            if (toJson.converter().equals(ToJson.NULL_CONVERTER.class)) {
                valuesHash.put(memberName, run(memberValue, toJson.includeOnly()));
            } else {
                JsonFieldConverter converter = getJsonFieldConverter(toJson.converter());
                valuesHash.put(memberName, run(converter.convert(memberValue), toJson.includeOnly()));
            }
        }
    }

    private Class fixHibernateProxyClass(Class clazz) {
        if (clazz.getName().indexOf(clazz.getSuperclass().getName() + HIBERNATE_PROXY_CLASS_POSTFIX) == 0) {
            return clazz.getSuperclass();
        }

        return clazz;
    }

    private Map<String, Boolean> buildIncludeOnlyHash(String[] includeOnly) {
        if ((includeOnly == null) || (includeOnly.length == 0)) return null;

        Map<String, Boolean> result = new HashMap<String, Boolean>();
        for (String key : includeOnly) result.put(key, true);

        return result;
    }

    private HashMap<String, String> getValuesToConvertHash(Object obj, String[] includeOnly) {
        Map<String, Boolean> includeOnlyHash = buildIncludeOnlyHash(includeOnly);
        HashMap<String, String> valuesHash = new HashMap<String, String>();
        Class objectClass = fixHibernateProxyClass(obj.getClass());
        Field[] fields = getAllFields(objectClass);
        Method[] methods = objectClass.getMethods();

        fillValuesHashWithMemberData(obj, fields, valuesHash, includeOnlyHash);
        fillValuesHashWithMemberData(obj, methods, valuesHash, includeOnlyHash);

        return valuesHash;
    }

    private String encodeObjectsCollection(Collection objects) {
        if (objects.size() == 0) return EMPTY_ARRAY;

        StringBuffer result = new StringBuffer("[");

        for (Object obj : objects) {
            result.append(run(obj)).append(",");
        }

        StringUtil.removeLastChar(result);
        return result.append("]").toString();
    }

    private String encodeObjectsArray(Object objects) {
        int length = Array.getLength(objects);

        if (length == 0) return EMPTY_ARRAY;

        StringBuffer result = new StringBuffer("[");

        for (int i = 0; i < length; ++i) {
            result.append(run(Array.get(objects, i))).append(",");
        }

        StringUtil.removeLastChar(result);
        return result.append("]").toString();
    }

    private String encodeObjectsMap(Map objects) {
        if (objects.size() == 0) return EMPTY_OBJECT;

        StringBuffer result = new StringBuffer("{");

        for (Object key : objects.keySet()) {
            result.append(StringUtil.escape(key.toString())).append(":")
                .append(run(objects.get(key))).append(",");
        }

        StringUtil.removeLastChar(result);
        return result.append("}").toString();
    }

    private String encodeSingleObject(Object obj, String[] includeOnly) {
        if ( obj == null ) return NULL_VALUE;

        PrimitiveClassType primitiveClassType = getPrimitiveClassType(obj.getClass());

        if ( primitiveClassType == PrimitiveClassType.SIMPLE ) {
            return obj.toString();
        } else if ( primitiveClassType == PrimitiveClassType.ESCAPABLE ) {
            return StringUtil.escape(obj.toString());
        } else if ( primitiveClassType == PrimitiveClassType.DATE ) {
            return StringUtil.escape(dateFormatter.format(obj));
        }

        StringBuffer result = new StringBuffer("{");
        HashMap<String, String> valuesHash = getValuesToConvertHash(obj, includeOnly);

        if ( valuesHash.size() == 0 ) {
            if (primitiveClassType == PrimitiveClassType.ENUM) {
                return StringUtil.escape(obj.toString());
            }

            return EMPTY_OBJECT;
        }

        if (primitiveClassType == PrimitiveClassType.ENUM) {
            valuesHash.put("enumValue", StringUtil.escape(obj.toString()));
        }

        for (Map.Entry<String, String> entry : valuesHash.entrySet()) {
            result.append(StringUtil.escape(entry.getKey())).append(":").append(entry.getValue()).append(",");
        }

        StringUtil.removeLastChar(result);
        return result.append("}").toString();
    }

    private String run(Object obj, String[] includeOnly) {
        if ( obj == null ) return NULL_VALUE;

        if (obj instanceof Collection ) {
            return encodeObjectsCollection((Collection) obj);
        } else if (obj.getClass().isArray()) {
            return encodeObjectsArray(obj);
        } else if (obj instanceof Map ) {
            return encodeObjectsMap((Map) obj);
        }

        return encodeSingleObject(obj, includeOnly);
    }

    public String run(Object obj) {
        return run(obj, null);
    }

    public void setCurrentProfile(String currentProfile) {
        this.currentProfile = currentProfile;
    }

}
